/* ***** BEGIN LICENSE BLOCK *****
 *
 * TakeControl Firefox extension
 * Copyright (C) 2006  Dafydd Jones <takecontrol.extension@googlemail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 *
 * ***** END LICENSE BLOCK ***** */

#include "TkctrlConstants.jsx"

const DEBUG = true;             /* set to true to enable debug messages */
const VERBOSE = false;  /* set to true to enable verbose debug messages */

const TKCTRL_STORE = "tkctrl.sqlite";

const CATMAN_CID = "@mozilla.org/categorymanager;1";
const TIMER_CID = "@mozilla.org/timer;1";
const OBSERVICE_CID = "@mozilla.org/observer-service;1";
const WINWATCH_CID = "@mozilla.org/embedcomp/window-watcher;1";

/** returns the date in the format YYYYMMDD */
Date.prototype.getW3CDate = function () {       
        var month = this.getMonth() + 1;
        month = ((month >= 0 && month <= 9) ? "0" : "") + month;
        var day = this.getDate().toString();
        day = ((day >= 0 && day <= 9) ? "0" : "") + day;
        return this.getFullYear().toString() + "-" + month + "-" + day;
}

/****************************
 * XPCOM components
 ****************************/
function TakeControlService() {
        this._timers =  {};
        this._store =   null;
        this._storeFilePath = null;
        this._paused =  false;
        this._activeLocation = TakeControlService.nullDOMLocation;
        this._database = null;
        this._policy = null;
}
TakeControlService.prototype = {
        CID: Components.ID("{49fff695-1be2-4600-958c-8b4b848c7e38}"),
        className: TKCTRL_SERVICE_NAME,
        contractID: TKCTRL_SERVICE_CID,

        get storeFilePath() { return this._storeFilePath },

        get siteDuration() {
                return this._store.siteDuration;
        },

        get totalDuration() {
                return this._store.totalDuration 
        },

        get paused() { return this._paused },
        set paused(x) {
                debug("paused = " + x);
                this._paused = x;
                this._notify(TKCTRL_DISPLAY);
        },

        get active() { return this._store.active },
        set active(x) {         
                this._store.active = x;
                this._notify(TKCTRL_DISPLAY);
        }
}
TakeControlService.prototype.observe = function (subject, topic, data) {
        switch (topic) {
                case "xpcom-startup":
                        debug("xpcom-startup");

                        var obsSvc = CC[OBSERVICE_CID].getService(CI.nsIObserverService);
                        obsSvc.addObserver(this, "profile-after-change", true);
                        obsSvc.addObserver(this, "quit-application", true);
                        
                        break;

                case "profile-after-change":
                        debug("profile-after-change");

                        this._setDates();
                        this._storeFilePath = this._initFilePath(TKCTRL_STORE);
                        this._database = new Database(this.storeFilePath);
                        debug("Database: " + this._database);
                        this._store = new Store(this._today, this._database);
                        debug("Store: " + this._store);
                        this._policy = new Policy();
                        debug("Store: " + this._policy);

                        //TODO work out more elegant way of initialising these timers
                        this._timers.quarter = CC[TIMER_CID].createInstance(CI.nsITimer);
                        this._timers.quarter.init(this, 250, this._timers.quarter.TYPE_REPEATING_SLACK);
                        this._timers.second = CC[TIMER_CID].createInstance(CI.nsITimer);
                        this._timers.second.init(this, 1000, this._timers.second.TYPE_REPEATING_SLACK);
                        this._timers.minute = CC[TIMER_CID].createInstance(CI.nsITimer);
                        this._timers.minute.init(this, 60000, this._timers.minute.TYPE_REPEATING_SLACK);
                        break;

                case "timer-callback":
                        switch (subject) {
                                case this._timers.quarter:
                                        verbose("quarter");
                                        //every quarter second, check if the active location has changed
                                        if (this._locationChanged()) {
                                                this._store.changeSite(this._activeLocation);
                                                this._notify(TKCTRL_DISPLAY); 
                                        }                                               
                                        break;

                                case this._timers.second:
                                        verbose("second");                                              

                                        //only increment time if not paused
                                        if (!this.paused) {                                                     
                                                this._store.increment();                                        
                                        }

                                        this._notify(TKCTRL_DISPLAY);
                                        break;

                                case this._timers.minute:
                                        debug("minute");
                                        //check for new day
                                        var nowLocal = new Date();
                                        if (nowLocal.getTime() >= this._tomorrowDate.getTime()) {
                                                this._triggerDateChange();
                                        }

                                        //each minute we save the stats
                                        this._store.flush();
                        }
                        break;

                case "quit-application":
                        debug("quit-application");
                        for each (var timer in this._timers) { timer.cancel() };

                        this._store.flush();

                        var obsSvc = CC[OBSERVICE_CID].getService(CI.nsIObserverService);
                        obsSvc.removeObserver(this, "profile-after-change");
                        obsSvc.removeObserver(this, "quit-application");
        }
}

TakeControlService.prototype.shouldLoad = function TCS_shouldLoad(contentType, contentLocation) 
{
    if (contentType != CI.nsIContentPolicy.TYPE_DOCUMENT) {        
        return CI.nsIContentPolicy.ACCEPT;
    } else {
        switch (contentLocation.scheme) {
        case "http":
        case "https":
        case "file":
            debug("shouldLoad from: " + contentType + " " + contentLocation.spec);
            return this._policy.shouldLoad(new Date()) ? CI.nsIContentPolicy.ACCEPT :
                                                         CI.nsIContentPolicy.REJECT_OTHER;
        default:
            return CI.nsIContentPolicy.ACCEPT;
        }
    }
}

TakeControlService.prototype.shouldProcess = function TCS_shouldProcess() {
    return CI.nsIContentPolicy.ACCEPT;
}


TakeControlService.prototype.decrement = function TCS_decrement(interval) {
        this._store.decrement(interval);
        this._notify(TKCTRL_DISPLAY);
}

TakeControlService.prototype.increment = function TCS_increment(interval) {
        this._store.increment(interval);
        this._notify(TKCTRL_DISPLAY);
}

TakeControlService.prototype.QueryInterface = function (iid) {
        verbose("QueryInterface(" + iid + ")");
        if (!iid.equals(CI.ITakeControlService)
           && !iid.equals(CI.nsISupports)
           && !iid.equals(CI.nsIObserver)
           && !iid.equals(CI.nsIContentPolicy)
           && !iid.equals(CI.nsISupportsWeakReference) )
            throw CR.NS_ERROR_NO_INTERFACE;
        return this;
}

TakeControlService.prototype._notify = function TCS__notify(topic) {
        CC[OBSERVICE_CID].getService(CI.nsIObserverService)
                                                .notifyObservers(null, topic, null);
}

TakeControlService.prototype._locationChanged = function TCS__locationChanged() {
        if (!this.winWatch) {
                this.winWatch = CC[WINWATCH_CID].getService(CI.nsIWindowWatcher);
        }

        var newLocation;
        var locationChanged = false;
        var win = this.winWatch.activeWindow;
        
        if (win && win.content)  {
                //create a JS location object, due to vagaries of XPConnect
                newLocation = new Location(win.content.location);
        }
        else {
                // active window is not a browser
                // create null location object
                newLocation = TakeControlService.nullDOMLocation;
        }
        verbose("comparing " + this._activeLocation.href + " to " + newLocation.href);
        if (newLocation.href !== this._activeLocation.href) {
                debug("location changed from: "  + this._activeLocation +
                                " to: " + newLocation);
                this._activeLocation = newLocation;
                locationChanged = true;
        }
        
        return locationChanged;
}

TakeControlService.prototype._setDates = function TCS__setDates() {
        var now = new Date();
        this._today = now.getW3CDate();
        this._tomorrowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
        debug("today = " + this._today + "; tomorrow = " + this._tomorrowDate.getW3CDate());
}

TakeControlService.prototype._triggerDateChange = function TCS__triggerDateChange() {
        debug("switched to new day");
        this._setDates();
        this._store.triggerDateChange(this._today);
        this._notify(TKCTRL_DISPLAY);
}

TakeControlService.prototype._initFilePath = function TCS__initFilePath(storeFileName) {
        var dirService = CC["@mozilla.org/file/directory_service;1"].getService(CI.nsIProperties);
        var path = dirService.get("ProfD", Components.interfaces.nsIFile);
        path.append(storeFileName);
        verbose(path + " " + path.path);

        debug("Database file path: " + path.path);
        return path; //return the nsIFile object
}

function Location(nsIDOMLocation) {
        try {
                this.href = nsIDOMLocation.href;
                this.protocol = nsIDOMLocation.protocol;
                this.port = nsIDOMLocation.port;
                this.host = nsIDOMLocation.host;
                this.hostname = nsIDOMLocation.hostname;
                this.pathname = nsIDOMLocation.pathname;
                this.search = nsIDOMLocation.search;
        }
        catch (e) {
                //any exception here means we create dummy values
                this.protocol = "about:";
                this.href = "about:takecontrol";
        }
}
Location.prototype.toString = function () { return this.href; }

TakeControlService.nullDOMLocation = {
        protocol: "about:",
        href: "about:takecontrol",
        toString: function () { return this.href }
}

/***************
 * module object
 ***************/
var takeControlModule = {};

takeControlModule.registerSelf = function (compMgr, fileSpec, loaderStr, type) {
    debug("registering component");
    compMgr = compMgr.QueryInterface(CI.nsIComponentRegistrar);

    for each (var obj in this._objects) {
        for (var prop in obj) verbose(prop + ":" + obj[prop]);
        compMgr.registerFactoryLocation(obj.CID, obj.className,
                                        obj.contractID, fileSpec, loaderStr, type);
    }

    var catMan = CC[CATMAN_CID].getService(CI.nsICategoryManager);
    catMan.addCategoryEntry("xpcom-startup",
                            TKCTRL_SERVICE_NAME,
                            TKCTRL_SERVICE_CID,
                            true,
                            true);
    catMan.addCategoryEntry("content-policy",
                            TKCTRL_SERVICE_CID,
                            TKCTRL_SERVICE_CID,
                            true,
                            true);
    debug("done registering component");
}

takeControlModule.unregisterSelf = function(compMgr, location, loaderStr) {
        compMgr.QueryInterface(CI.nsIComponentRegistrar);
    for each (var obj in this._objects) {
      compMgr.unregisterFactoryLocation(obj.CID, location);
    }

        var catMan = CC[CATMAN_CID].getService(CI.nsICategoryManager);
        catMan.deleteCategoryEntry("xpcom-startup", TKCTRL_SERVICE_NAME, true);
        catMan.deleteCategoryEntry("content-policy", TKCTRL_SERVICE_CID, true);
}

takeControlModule.getClassObject = function (compMgr, cid, iid) {
    if (!iid.equals(CI.nsIFactory))
        throw CR.NS_ERROR_NOT_IMPLEMENTED;

        for (var i in this._objects) {
                if (cid.equals(this._objects[i].CID))
                return this._objects[i].factory;
    }

    return CR.NS_ERROR_NO_INTERFACE;
}

takeControlModule.canUnload = function (compMgr) {
    debug("unloading component");
    return true;
}

takeControlModule._objects = {}

/* array of objects we want to XPCOMify */
var objects = [TakeControlService];

/* module entrypoint */
function NSGetModule(compMgr, fileSpec) {
        debug("NSGetModule");
        for (var i in objects) {
                //create a new set of XPCOM objects with factories
                takeControlModule._objects[i] = new FactoryHolder(objects[i]);
        }
    return takeControlModule;
}

function FactoryHolder(obj) {
    this.CID        = obj.prototype.CID;
    this.contractID = obj.prototype.contractID;
    this.className  = obj.prototype.className;
    this.factory = {
        createInstance: function(outer, iid) {
                        verbose("createInstance(" + iid + ")");
                        if (outer != null) throw CR.NS_ERROR_NO_AGGREGATION;
                        return (new this.constructor).QueryInterface(iid);
                }
        };
        this.factory.constructor = obj;
        for (var prop in obj) verbose(prop + " : " + obj[prop]);
}

/* logging functions */
var debug = DEBUG ? function (s) { dump("tkctrl xpcom: " + s + "\n"); } : function (s) {}
var verbose = VERBOSE ? function (s) { dump("tkctrl xpcom: " + s + "\n"); } : function (s) {}

#include "Store.jsx"
#include "Database.jsx"
#include "Policy.jsx"
